﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Xml;

namespace EmbeddedSensorCloud.Plugin.Navigation
{
    /// <summary>
    /// An object capable of reading a XML file using a stream. The SaxParser will call delegates to let the caller handle the found data.
    /// </summary>
    public class SaxParser
    {
        #region Delegates(Properties)/Events

        /// <summary>
        /// Will be called when the start of the XML document is found.
        /// </summary>
        public Action StartDocumentAction { get; set; }
        /// <summary>
        /// Will be called when a start (&lt;...&gt;) or start-end (&lt;.../&gt;) element is found.
        /// It contains the element's name and it's attributes.
        /// </summary>
        public Action<string, Dictionary<string, string>> StartElementAction { get; set; }
        /// <summary>
        /// Will be called when simple characters are found (this includes whitespaces if not ignored).
        /// It contains all the characters found until the next significant element.
        /// </summary>
        public Action<string> CharactersAction { get; set; }
        /// <summary>
        /// Will be called when the end-tag of an element (&lt;/...&gt;) is found.
        /// It contains the element's name which end is found.
        /// </summary>
        public Action<string> EndElementAction { get; set; }
        /// <summary>
        /// Will be called when the end of the document was found.
        /// </summary>
        public Action EndDocumentAction { get; set; }

        #endregion

        public string FilePath { get; set; }
        public bool IgnoreWhitespaces { get; set; }
        public bool IsAsynchronous { get; set; }

        /// <summary>
        /// Indicates whether the asynchronous work has finished yet.
        /// </summary>
        public bool FinishedWork { get; private set; }

        #region Constructor

        /// <summary>
        /// Creates a new SaxParser instance with the given file path and optional parameters to decide whether the parser should run asynchronously and ignore whitespaces.
        /// </summary>
        /// <param name="filePath">The path to the XML-File.</param>
        /// <param name="isAsynchronous">Indicates whether whitespaces should be ignored.</param>
        /// <param name="ignoreWhitespaces">Indicates whether the parsing procedure should work asynchronously.</param>
        public SaxParser(string filePath, bool isAsynchronous = false, bool ignoreWhitespaces = true)
        {
            this.FilePath = filePath;
            this.IsAsynchronous = isAsynchronous;
            this.IgnoreWhitespaces = ignoreWhitespaces;
            this.FinishedWork = true;
        }

        #endregion

        /// <summary>
        /// Opens a stream to the given file path and begins reading it. Depending on the 'isAsynchronous' option while creating the instance, this procecure may work asynchronously.
        /// </summary>
        /// <exception cref="System.ArgumentNullException" />
        /// <exception cref="System.IO.FileNotFoundException" />
        /// <exception cref="System.UriFormatException" />
        /// <exception cref="System.Xml.XmlException" />
        /// <exception cref="System.InvalidOperationException" />
        /// <exception cref="System.Threading.ThreadStateException" />
        /// <exception cref="System.OutOfMemoryException" />
        public void StartParsing()
        {
            if (this.FinishedWork)
            {
                if (!File.Exists(this.FilePath))
                { throw new FileNotFoundException(); }

                if (!this.IsAsynchronous)
                {
                    ParseStream();
                }
                else
                {
                    Thread worker = new Thread(ParseStream);
                    worker.Start();
                }
            }
        }

        /// <summary>
        /// Opens a stream to the specified file and walks through it.
        /// </summary>
        private void ParseStream()
        {
            this.FinishedWork = false;

            try
            {
                XmlReaderSettings settings = InitializeSettings();

                using (XmlReader reader = XmlReader.Create(this.FilePath, settings))
                {
                    Dictionary<string, string> attributes;

                    while (reader.Read())
                    {
                        switch (reader.NodeType)
                        {
                            case XmlNodeType.XmlDeclaration:
                                StartDocument_Fire();
                                break;
                            case XmlNodeType.Element:
                                attributes = new Dictionary<string, string>();
                                if (reader.HasAttributes)
                                {
                                    while (reader.MoveToNextAttribute())
                                    { attributes[reader.Name] = reader.Value; }
                                    reader.MoveToElement();
                                }
                                StartElement_Fire(reader.Name, attributes);
                                break;
                            case XmlNodeType.EndElement:
                                EndElement_Fire(reader.Name);
                                break;
                            case XmlNodeType.Text:
                                Characters_Fire(reader.Value);
                                break;
                            case XmlNodeType.Whitespace:
                                if (!this.IgnoreWhitespaces)
                                {
                                    Characters_Fire(reader.Value);
                                }
                                break;
                        }
                    }
                    EndDocument_Fire();
                }
            }
            finally
            {
                this.FinishedWork = true;
            }
        }

        /// <summary>
        /// Creates a new instance of XmlReaderSettings a sets a few parameters.
        /// </summary>
        /// <returns>A new instance of XmlReaderSettings with set parameters.</returns>
        private XmlReaderSettings InitializeSettings()
        {
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.ConformanceLevel = ConformanceLevel.Fragment;
            settings.IgnoreComments = true;
            settings.IgnoreWhitespace = this.IgnoreWhitespaces;
            settings.DtdProcessing = DtdProcessing.Ignore;
            settings.CloseInput = true;
            
            return settings;
        }

        #region Delegate/Event-FireMethods

        private void StartDocument_Fire()
        {
            if (this.StartDocumentAction != null)
            { this.StartDocumentAction(); }
        }

        private void StartElement_Fire(string name, Dictionary<string, string> attributes)
        {
            if (this.StartElementAction != null)
            { this.StartElementAction(name, attributes); }
        }

        private void Characters_Fire(string value)
        {
            if (this.CharactersAction != null)
            { this.CharactersAction(value); }
        }

        private void EndElement_Fire(string name)
        {
            if (this.EndElementAction != null)
            { this.EndElementAction(name); }
        }

        private void EndDocument_Fire()
        {
            if (this.EndDocumentAction != null)
            { this.EndDocumentAction(); }
        }

        #endregion
    }
}
